Skip to content

perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazy marketplace namespace - #30640

Draft
objectstack-fleet[bot] wants to merge 15 commits into
mainfrom
claude/project-thread-9mg01c
Draft

objectstack-fleet[bot] wants to merge 15 commits into
mainfrom
claude/project-thread-9mg01c

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #30588

Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: none — no skills/, .claude/ or docs/ text names the moved keys or teaches the pack shape (the 17 marketplace. hits there are marketplace.zod.ts and marketplace prose)

维护者速读

  • 改了什么:应用市场两张页面(目录页、应用详情页)用到的文案从十个语言包里搬出,放进一个按需加载的 marketplace 文案包;打开市场页时才下载当前语言那一份(外加英文兜底),下载完成前页面显示原有的加载骨架,下载失败显示控制台自己的「出错了 / 重试」。Studio 软件包页也会用到的那部分文案(卸载、未加载徽标、依赖缺失与协议提示)留在语言包里不动。文字本身一个字没改。
  • 为什么改:Console first load: the English language pack has 63 bytes left under its size ceiling, so any card that adds English console text fails CI #30588。控制台首屏必载的英文语言包 i18n-locale-en 已到 59,951 / 60,000 gz,只剩 49 字节,挡住了 feat(console): a marketplace app's Install confirms what it adds before it installs #30545、feat(console): an administrator's first-run checklist on Home replaces the currency and time zone dialog #30552 两个要加市场文案的 PR;上限不许抬。现在读数 57,003,余量 2,997(合入 main 之后的 head 54db079b)。
  • 风险与回滚:只改前端与构建分组;首屏总量 2,204,342 → 2,201,442 gz,没有新增首屏块,十份市场文案、加载器和加载框架都不在首屏。直接读语言包对象的代码(包入口的 en、@object-ui/i18n/locales/CODE)不再含被搬走的键,属 TypeScript 层面的收窄,changeset 写明了迁移。回滚即还原本 PR。
  • 先看:ui/packages/i18n/src/namespaces/marketplace/index.ts(加载与就绪判断)、ui/packages/app-shell/src/console/marketplace/MarketplaceNamespaceGate.tsx(页面框架)、ui/apps/console/vite.config.ts 的 i18n-ns-marketplace* 与 marketplace-namespace-gate 分组(为什么两组都要 includeDependenciesRecursively: false)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.namespaceGate-30588.test.tsx packages/app-shell/src/console/marketplace/__tests__/marketplaceNamespaceBoundary.test.ts packages/i18n/src/__tests__/marketplace-namespace-lazy.test.tsx,预期 14 个用例全绿(中文页先骨架后中文、从不出现英文或原始键;切换语言时不误弹示例数据横幅)。
  • 需要你做的:改动约 6,600 行(41 个文件,+4,338 / −2,294)(多为十个语言文件里整段搬移),超过 3,000 行人工合并线,请在席位复核通过后批准合并。

What changes

  • A lazy marketplace namespace. ui/packages/i18n/src/namespaces/marketplace/CODE.ts (ten files) are partial packs: the same shape as src/locales/CODE.ts, holding the marketplace group cut out of that pack, text unchanged (each language's pack merged with its catalogue is deep-equal to its pack at bdbaf80c). index.ts loads them through ten literal import()s and exports useMarketplaceNamespace(), loadMarketplaceCatalogue(), isMarketplaceCatalogueLoaded(), resolveMarketplaceLanguage() and the namespace constants. New published subpath @object-ui/i18n/namespaces/marketplace.
  • How a page waits. useMarketplaceNamespace() reads the i18next instance react-i18next serves, works out the two catalogues the language needs (its own, zh-CN served by zh, plus en, the packs' fallbackLng), merges the resident ones into the instance deep and without overwriting (a host's own resources win, as they do over the packs), and answers loading / ready / failed with a retry(). It records per instance which catalogues it installed, so a host resource that spells one marketplace.* key does not stop the rest of that language's catalogue. A catalogue the all-ten door registered (registerBuiltInLocales()) is used without a fetch. A failed fetch stays failed until retry() (or a later mount) asks again: no loop.
  • The page frame. App-shell's MarketplaceNamespaceGate wraps the bodies of MarketplacePage and MarketplacePackagePage: the existing copy-free skeleton (MarketplaceResolving) while loading and after a language switch, the console's resident console.errors.somethingWentWrong with console.errors.tryAgain when a catalogue cannot be fetched, the page once ready. The skeleton after a language switch unmounts the page body while the visitor stays on the page, so the frame also says when it is holding the page back (marketplaceFrameHolding.ts), and SampleDataLeaveBanner's unmount cleanup asks it before raising the banner that follows an installer off the package page.
  • What stays in the packs. Studio's packages page (views/metadata-admin/PackagesPage.tsx), outside the marketplace route, renders MarketplaceRowActions, MarketplaceNotLoaded and the dependencyMissing / protocolCheckSkipped readers. Their keys stay resident in all ten packs: marketplace.installedAdditiveNote, marketplace.notLoaded.*, marketplace.action.uninstall|uninstalling|details, marketplace.uninstall.confirm|confirmNotLoaded|successInList|successNotLoaded, marketplace.install.dependencyMissing.*, marketplace.install.protocolCheckSkipped.* (19 leaves in en). The new marketplaceNamespaceBoundary.test.ts holds that set to what those files read.
  • The all-ten door stays complete. @object-ui/i18n/locales now hands out each language as its pack deep-merged with its namespace catalogue and refuses a key defined in both, so all-locales-key-parity and every suite walking builtInLocales still walk the moved keys, and registerBuiltInLocales() still makes every string resident. The package entry's en is the resident pack alone.
  • The i18n gates learn the namespace. check-i18n-call-site-keys.mjs (collectEnKeys, hence check-i18n-dead-keys.mjs too) and check-i18n-en-drift.mjs read namespaces/marketplace/CODE.ts beside each pack, refuse a key defined in both places, and keep the namespace file optional for fixture roots. No other gate rule changed.
  • Chunking. ui/apps/console/vite.config.ts gains i18n-ns-marketplace (the loader) and ten i18n-ns-marketplace-CODE groups, plus marketplace-namespace-gate (the frame). The loader's and the frame's groups take includeDependenciesRecursively: false; the comment there gives the two measured reasons (the default stole locales/registry.ts and the preload helper into an eager loader chunk; the frame otherwise lands in the chunk the two pages share with the eager packages page).
  • Ceiling untouched. PER_CHUNK_GZIP_CEILINGS and PER_CHUNK_BASELINE are unchanged; the new reading is recorded in prose beside 'i18n-locale-en': 60_000. locales/registry.ts and the pack file names are unchanged.
  • Changeset. .changeset/30588-marketplace-lazy-namespace.md, @object-ui/i18n and @object-ui/app-shell patch, BREAKING with its migration, ADR-0087 not-required (runtime-interface-only ui/packages/i18n/src/locales/en.ts#TranslationKeys).

Branch: the harness-assigned claude/project-thread-9mg01c, not claude/issue-30588-*; the card is #30588.

Tests

  • New packages/i18n/src/__tests__/marketplace-namespace.test.ts (56 cases): ten literal import() loaders and no static catalogue import (read by the TypeScript parser); every catalogue holds only the marketplace group and the probe key, no pack holds the probe key, no leaf is in both a pack and its catalogue; the all-ten door's languages are exactly pack plus catalogue; the entry's en lacks the moved keys; language resolution; the loader refuses an unknown code and memoises.
  • New packages/i18n/src/__tests__/marketplace-namespace-lazy.test.tsx (5 cases, registry re-registered as packs alone): zh-CN answers loading first, then every render of the probe is Chinese (no English, no raw key) and only zh and en were fetched; a switch to ja waits again; sv is served by en; a host resource for marketplace.title wins and marketplace.refresh still arrives in Chinese; a failing ko chunk answers failed once, makes no second request on its own, and retry() loads it.
  • New packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.namespaceGate-30588.test.tsx (4 cases, a MutationObserver records every text node the page adds): the catalogue page in zh-CN and the package page in de show the skeleton, then their language, and never the English text or a raw key; an ar chunk that fails shows the Arabic resident error words, and Try again fetches it and renders Arabic; on a package page with a sample-data record from this session, a switch from zh-CN to ja raises no banner in any language, and leaving the page raises it in Japanese. Before the frame said it was holding, that case failed: the switch raised the banner in Chinese while the installer was still on the page (expected … not to contain '"Service Desk" 中有示例数据。').
  • New packages/app-shell/src/console/marketplace/__tests__/marketplaceNamespaceBoundary.test.ts (5 cases): finds every module outside console/marketplace/ that imports into it, holds that both pages render under the frame, and that every marketplace.* key spelled by a file reachable from an ungated door is a resident pack key.
  • Gate tests: scripts/__tests__/check-i18n-call-site-keys.test.ts and check-i18n-en-drift.test.ts gain the namespace cases (a group moved into the namespace is no drift; a change inside it is; a key in both is refused).
  • Ablation (the dispatch's). With the commit in place, scripts/ablation-replace.mjs replaced MarketplacePage's MarketplaceNamespaceGate wrapper with a bare fragment (the page no longer loads the namespace): the frame test went red, 2 failed / 3, the first frame reading expected 'marketplace.titlemarketplace.subtitle…' not to match /\bmarketplace\.[A-Za-z]/. The same on MarketplacePackagePage: 1 failed / 3. Both restored by the tool's trap: blob equals HEAD, git diff HEAD empty.
  • The boundary test catches the gap it was written for. At the packs of 69ab56e1 (before the Studio keys went back), it failed 2 / 5, naming the eleven MarketplaceRowActions.tsx keys.

Acceptance notes

  • Bundle readings (console CI=true vite build, apps/console/dist/eager-closure.json): i18n-locale-en 59,951 at bdbaf80c → 57,010 at 17e15c7f (ceiling 60,000, headroom 49 → 2,990). Eager closure 2,204,342 → 2,201,442 gz, 103 → 103 eager chunks with the same names on both sides; no other chunk moved by more than 40 bytes. No i18n-ns-marketplace* chunk and no marketplace-namespace-gate chunk is eager. The ten catalogue chunks read 3,724 (en) to 5,048 (ru) gz each, the loader 1,315 gz, the frame 704 gz, all lazy. The prose beside the ceiling records 2,201,440, read at 5986ae70; the 2 bytes are chunk-hash noise.
  • Screenshots (live stack, console via vite): catalogue page in zh-CN (shot) and de (shot); package page in zh-CN (shot) and de (shot), 1440 wide. A Playwright run recorded every text node each page added on the way: no raw marketplace. key and no English title or subtitle in any of them, no page error. The language files (zh and en, de and en) were requested only once the marketplace page opened; the dev server serves the loader module itself at boot, since dev has no chunks (the production readings above are the lazy ones). Shots committed at af733e30 and removed from the tree by the next commit; they predate 17e15c7f, which changes only when the sample-data banner is raised.
  • Language switch while a page is open shows the skeleton until the new language's catalogue lands, which unmounts the page body: a half-filled install dialog is lost on a switch, and the page reads its package again. Chosen over keeping the body mounted, which would show English for a moment. The one unmount cleanup in these pages that means the visitor left, the sample-data banner, checks the frame first (fixed in this PR, see Tests).
  • For i18n phase 2 (Console UI): ui/packages/i18n reads PLATFORM_LOCALES, adds zh-Hant, and moves pack keys to table tags #30511 / PR feat(i18n)!: the Console's language packs follow the platform locale table, keyed by its tags, and Traditional Chinese (zh-Hant) ships #30595: that card renames packs to zh-Hans / zh-Hant / pt-BR; after this lands it adds the matching namespaces/marketplace/CODE.ts files and loader entries (the shape test pins one loader per BUILT_IN_LANGUAGE_CODES entry, so it will say so).
  • For PR feat(console): a marketplace app's Install confirms what it adds before it installs #30545 / PR feat(console): an administrator's first-run checklist on Home replaces the currency and time zone dialog #30552: their new marketplace.install.preview.* and related keys belong in the namespace catalogues, unless Studio's packages page reads them.

Verification

All on head 17e15c7f unless noted. Lock readings are the lock's own VERDICT lines.

Re-run on 54db079b (origin/main merged at 64857f64; PR #30653's new purgeConfirmAtMost wording carried into namespaces/marketplace/<lang>.ts in all ten languages, one test stub for its countLocalSampleData read; os-dev-report 6671129796): every pack deep-merged with its catalogue deep-equals main's pack at 47c457b23; console/marketplace + i18n vitest 150 files, 1980 tests passed; ui scripts/ 170 files, 4979 tests; check:i18n-keys, check:i18n-dead-keys, check-i18n-en-drift.mjs --base origin/main exit 0; i18n-locale-en 57,003 gz (headroom 2,997); 57/57 derived gates exit 0; full dogfood 421 files, 3513 tests passed. The readings below are the 17e15c7f ones.

  • pnpm --dir ui exec vitest run packages/app-shell/src/console/marketplace packages/i18n: 150 files passed, 1960 tests passed, 13 skipped.
  • pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin (Studio's packages page and the rest of that view): run with the marketplace directory at ae5aac72, 780 files, 8133 passed, 1 skipped. Later commits touch only the frame, the namespace hook, the banner and tests; the boundary test holds that nothing outside the marketplace pages reaches them.
  • pnpm --filter @object-ui/app-shell run type-check (source and tests): exit 0. pnpm --filter @object-ui/console run type-check then build:plugin: exit 0. pnpm --filter @object-ui/i18n run type-check: exit 0 (no i18n source change after f10a7fdf).
  • Reverse check on the narrowed type (one-off file in app-shell, against the rebuilt @object-ui/i18n dist, removed after): en.marketplace.action.uninstall compiled and en.marketplace.title gave exactly one error, TS2339: Property 'title' does not exist; git status clean after.
  • pnpm --dir ui run check:i18n-keys (4,210 keys), check:i18n-dead-keys (report identical to base: same 94 entries), node scripts/check-i18n-en-drift.mjs --base bdbaf80c (0 en values changed): exit 0 each.
  • pnpm --dir ui check:eager-closure, pnpm --dir ui check:eager-locale-catalogues: exit 0; scripts/__tests__/check-eager-closure-budget.test.ts (174): exit 0.
  • node scripts/check-adr-0087-registration.mjs --base origin/main: verified runtime-interface-only on TranslationKeys.
  • eslint --no-inline-config on every changed source, test and script: 0 errors; the only warnings are pre-existing ones in the two page files.
  • node scripts/pm/dispatch-gates.mjs --commands: 57 derived families, the same list at 9cf51c22 and at 17e15c7f. All 57 exit 0; --ran reconciles 57 run, 0 NOT-MEASURED, 0 UNRUN. check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (seven root dists absent) and passed once they were built; pnpm --dir ui exec vitest run scripts/ passed on 17e15c7f (170 files, 4979 tests, 2 skipped). Re-run on 17e15c7f after the last commits: check:type-check-debt, check:type-check-coverage, check:nul-bytes, check:test-source-alias, check:cross-package-test-inputs, check:published-files, check:undeclared-dep-imports, check:sourcemap-no-sources-content, check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, all exit 0. Outside the derived list, pnpm --dir ui run check:new-line-citations: 0 new citations.
  • Full dogfood (the narrowing arm): pnpm exec turbo run build --filter='@objectstack/dogfood^...' (69 tasks), then pnpm --filter @objectstack/dogfood test under the lock on 17e15c7f: exit 0, Test Files 420 passed, 3 skipped (423); Tests 3496 passed, 36 skipped (3532). No failure.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgXDwFw71MwSG9FKYDQ

claude added 13 commits November 5, 2026 17:56
…packs into a marketplace namespace

Mechanical move: each pack's marketplace group, except the two install
families Studio's packages page reads, now lives in
namespaces/marketplace/<code>.ts. Merged back, every catalogue equals
the pack it came from (no key overlaps, no value changes).

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXDwFw71MwSG9FKYDQ
…pace before they render

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXDwFw71MwSG9FKYDQ
…uage

The call-site-keys, dead-keys and en-drift gates now walk the lazy
namespace's partial packs together with each pack, refuse a key defined
in both, and keep a namespace file optional for fixture roots.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXDwFw71MwSG9FKYDQ
…est the marketplace namespace

Studio's packages page draws marketplace install rows with
MarketplaceRowActions and MarketplaceNotLoaded, outside the marketplace
frame, so the eleven leaves those read go back into the ten packs. A
source census test in app-shell holds every marketplace key an ungated
file spells to the resident pack.

The namespace now tracks which catalogues each i18next instance was
given, so a host resource spelling one marketplace key no longer stops
the rest of that language's catalogue; a catalogue the all-ten door
registered is used without a fetch; retry shows the loading state again.

Adds the namespace's shape tests, its lazy runtime tests, and the
marketplace pages' frame test.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXDwFw71MwSG9FKYDQ
…r keys and English

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXDwFw71MwSG9FKYDQ
…without useTranslation

Read from react-i18next's context or its global instance and subscribe to
languageChanged directly, so a marketplace component rendered without a
provider does not warn NO_I18NEXT_INSTANCE.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXDwFw71MwSG9FKYDQ
…ame out of the eager closure

The loader's chunk group followed its imports and took the eager locale
registry and preload helper; the frame, shared by the two lazy pages,
sat in the chunk they share with the eager Studio packages page. Both
groups now hold only their own module, so the loader, the frame and the
ten catalogues are lazy and the eager closure changes only by the
i18n-locale-en chunk's reading, recorded beside its ceiling.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXDwFw71MwSG9FKYDQ
…ses no sample-data banner

The strings frame swaps the page body for its load skeleton while a new
language's catalogue arrives, which unmounted SampleDataLeaveBanner and raised
the banner that should follow the installer only once they leave the page.
The frame now says when it is holding the page back, and the banner's cleanup
asks it first.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXDwFw71MwSG9FKYDQ
@github-actions github-actions Bot added size/xl dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

Contract-review-claim: session_01L6bhgWn6zhNa4LL7VB1rtL · head 17e15c7 · 2026-11-05T19:57Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 17e15c7ffbcf57dc5fbe175dfc845bfbb20bb761
Local-runs: none
Scope: ui/packages/i18n/package.json, ui/packages/i18n/README.md, ui/packages/i18n/src/locales/index.ts, ui/packages/i18n/src/locales/en.ts, ui/packages/i18n/src/locales/zh.ts, ui/packages/i18n/src/locales/ja.ts, ui/packages/i18n/src/locales/ko.ts, ui/packages/i18n/src/locales/de.ts, ui/packages/i18n/src/locales/fr.ts, ui/packages/i18n/src/locales/es.ts, ui/packages/i18n/src/locales/pt.ts, ui/packages/i18n/src/locales/ru.ts, ui/packages/i18n/src/locales/ar.ts, ui/packages/i18n/src/namespaces/marketplace/index.ts, ui/packages/i18n/src/namespaces/marketplace/en.ts, ui/packages/i18n/src/namespaces/marketplace/zh.ts, ui/packages/i18n/src/namespaces/marketplace/ja.ts, ui/packages/i18n/src/namespaces/marketplace/ko.ts, ui/packages/i18n/src/namespaces/marketplace/de.ts, ui/packages/i18n/src/namespaces/marketplace/fr.ts, ui/packages/i18n/src/namespaces/marketplace/es.ts, ui/packages/i18n/src/namespaces/marketplace/pt.ts, ui/packages/i18n/src/namespaces/marketplace/ru.ts, ui/packages/i18n/src/namespaces/marketplace/ar.ts, ui/packages/i18n/src/tests/marketplace-namespace.test.ts, ui/packages/i18n/src/tests/marketplace-namespace-lazy.test.tsx, ui/packages/app-shell/src/console/marketplace/MarketplaceNamespaceGate.tsx, ui/packages/app-shell/src/console/marketplace/marketplaceFrameHolding.ts, ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx, ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx, ui/packages/app-shell/src/console/marketplace/SampleDataLeaveBanner.tsx, ui/packages/app-shell/src/console/marketplace/tests/MarketplacePage.namespaceGate-30588.test.tsx, ui/packages/app-shell/src/console/marketplace/tests/marketplaceNamespaceBoundary.test.ts, ui/apps/console/vite.config.ts, ui/scripts/check-eager-closure-budget.mjs, ui/scripts/check-i18n-call-site-keys.mjs, ui/scripts/check-i18n-dead-keys.mjs, ui/scripts/check-i18n-en-drift.mjs, ui/scripts/tests/check-i18n-call-site-keys.test.ts, ui/scripts/tests/check-i18n-en-drift.test.ts + .changeset/30588-marketplace-lazy-namespace.md (prose face, read for ②), ui/packages/i18n/src/index.ts, ui/packages/i18n/src/i18n.ts, ui/packages/i18n/src/locales/registry.ts, ui/packages/i18n/src/namespaces/studio/index.ts, ui/packages/app-shell/src/index.ts, ui/packages/app-shell/src/console/marketplace/MarketplaceResolving.tsx, ui/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx, ui/scripts/check-eager-locale-catalogues.mjs, scripts/pm/clause2-line.mjs, scripts/check-adr-0087-registration.mjs, docs/adr/0087-metadata-protocol-upgrade-contract.md, scripts/pm/check-governed-queue-guard.mjs, AGENTS.md, .claude/skills/pm-dispatch/SKILL.md, .claude/skills/pm-dispatch/references/contract-review.md

① Derived judgments

  1. What widens. One new published subpath, ./namespaces/marketplace (ui/packages/i18n/package.json:41-45, the ./namespaces/studio shape beside it), and the API behind it in ui/packages/i18n/src/namespaces/marketplace/index.ts: MarketplaceCatalogue (:81), MARKETPLACE_NAMESPACE (:84), MARKETPLACE_NAMESPACE_PROBE_KEY (:91), MARKETPLACE_LANGUAGE_CODES (:113), resolveMarketplaceLanguage() (:125), isMarketplaceCatalogueLoaded() (:158), loadMarketplaceCatalogue() (:168), MarketplaceNamespaceState (:261-270), useMarketplaceNamespace() (:296). Ten partial packs namespaces/marketplace/CODE.ts, each const en = { marketplace: {…} } with export default (en.ts:12-13), the shape the AST gates read. Correct: a widening on @object-ui/i18n only; @object-ui/app-shell's entry gains nothing (ui/packages/app-shell/src/index.ts:279-280 still exports only the two pages; MarketplaceNamespaceGate and marketplaceFrameHolding are not on the entry).
  2. What narrows. The root entry's en (ui/packages/i18n/src/index.ts:123, a re-export of ./locales/en.js), the type TranslationKeys = typeof en (ui/packages/i18n/src/locales/en.ts:5752, re-exported at src/index.ts:41, src/i18n.ts:336, locales/registry.ts:247, locales/index.ts:56, and by ui/packages/react/src/index.ts:111), and each pack module @object-ui/i18n/locales/CODE (en.ts diff: 245 changed lines, the marketplace group cut to 19 leaves — installedAdditiveNote, notLoaded.*, action.{uninstall,uninstalling,details}, uninstall.{confirm,confirmNotLoaded,successInList,successNotLoaded}, install.dependencyMissing.*, install.protocolCheckSkipped.* at en.ts:4928-4962; the nine other packs cut the same way, e.g. zh.ts). A t('marketplace.…') call keeps compiling (its key is string); what narrows is the object a direct reader indexes and the type a consumer derives from it. Correct reading, and the changeset names exactly these three.
  3. The @object-ui/i18n/locales door (the surface the claim did not list). ui/packages/i18n/src/locales/index.ts:35-54 now statically imports the ten packs AND the ten catalogues, and :89-98 hands out each language as complete(pack, catalogue) (:67-84, a deep merge that throws on a leaf defined in both, :75-77); builtInLocales (:104-115) and registerBuiltInLocales() (:156-160) carry the complete objects. Judged right: the module's own header names its two purposes — the parity/drift suites and an app that wants every catalogue resident — and both need every string; and registerBuiltInLocales() registering the namespace group is what lets residentCatalogue() (namespaces/marketplace/index.ts:145-155) serve a page without a fetch. Its key set is identical to main's (main's locales/index.ts:20-29 re-exported the pack modules), so this door neither widens nor narrows. Two consequences a reader should know, neither a finding: (a) builtInLocales.en is a new object, no longer identical to the entry en or to @object-ui/i18n/locales/en — no reader in the tree compares identity (head grep of builtInLocales outside i18n: console tests indexing .zh, .en, .auth …); (b) a duplicate key now throws at evaluation of the door, which the three gates in item 7 refuse first. It does not change the narrowing: the root en, TranslationKeys and ./locales/CODE still lose the subtree, and the door is the migration target the changeset names.
  4. The arm. Clause-②: yes (narrowing) on the PR body and in the changeset (line 8), Metadata-protocol: none on both. scripts/pm/clause2-line.mjs defines yes (narrowing) as "a diff that widens one surface and narrows another. Both facts are true and both are read" — exactly items 1 and 2. Correct spelling; no (narrowing) would deny the new subpath, yes (widening) would hide the cut.
  5. Pages and frame. MarketplacePage.tsx:313-320 and MarketplacePackagePage.tsx:162-169 wrap their bodies in MarketplaceNamespaceGate (gate: skeleton MarketplaceResolving while loading, resident console.errors.somethingWentWrong / tryAgain with retry when failed, children when ready, MarketplaceNamespaceGate.tsx:39-57, 59-75). MarketplaceResolving.tsx reads no t() (grep), so the loading frame is copy-free. SampleDataLeaveBanner.tsx:49-50 asks useMarketplaceFrameHolding() before raising the leave banner, and the gate sets that flag in a layout effect of the commit that swaps the body out (:42-44), which runs before that commit's passive cleanups — the ordering the guard relies on holds. Exported component signatures unchanged.
  6. Ceilings and the registry. PER_CHUNK_GZIP_CEILINGS['i18n-locale-en'] stays 60_000 (ui/scripts/check-eager-closure-budget.mjs:2239); the diff to that file is the ten-line comment at :2229-2238 only; PER_CHUNK_BASELINE is not in the diff; ui/packages/i18n/src/locales/registry.ts is not in the 41 files; pack file names unchanged. All as the card requires. The 59,951 → 57,010 reading and "same 103 eager chunk names" are the dev's builds (PR body, Acceptance notes); not re-measured here.
  7. Gates still see the moved keys. collectEnKeys reads locales/en.ts plus PACK_NAMESPACE_DIRS/en.ts (ui/scripts/check-i18n-call-site-keys.mjs:1329-1360, 1364-1426) and throws on a leaf in both or a leaf-vs-group clash (:1405-1413); the dead-keys report excludes hits inside PACK_DEFINITION_DIRS (check-i18n-dead-keys.mjs:468-474, 670); readPacks merges each language's partial pack and throws on a duplicate (check-i18n-en-drift.mjs:366-403); all-locales-key-parity walks builtInLocales, which is complete (item 3). The namespace en.ts is optional only when absent (:1358), in which case its keys are simply undefined and a call site asking for one is reported missing-key (gate test "reports a call site asking for a namespace key the namespace no longer defines") — nothing is weakened. The i18n-en-drift-baseline.json waiver of marketplace.searchPlaceholder is now read where the key lives (check-i18n-en-drift.test.ts:662-665 moved to readPacks).
  8. Chunking. ui/apps/console/vite.config.ts:1238-1277: i18n-ns-marketplace (loader, includeDependenciesRecursively: false), ten i18n-ns-marketplace-CODE groups, marketplace-namespace-gate (frame + holding flag, includeDependenciesRecursively: false), with the two measured reasons in the comment. check-eager-locale-catalogues.mjs keys on CATALOGUE_CHUNK_PREFIX = 'i18n-locale-' (:108), so the new i18n-ns-marketplace-* names are outside its census and cannot trip it.

② Semver level

  • @object-ui/i18n: patch, @object-ui/app-shell: patch (changeset :2-3). Correct under AGENTS.md Post-Task Checklist step 3: Metadata-protocol: none takes patch, and "a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition". Nothing a metadata author writes changes.
  • The body carries **BREAKING** (changeset :20) naming the three narrowed surfaces (entry en, TranslationKeys, @object-ui/i18n/locales/CODE) and the migration in prose: a component outside the marketplace pages calls useMarketplaceNamespace() and renders once ready; code reading the catalogue objects reads @object-ui/i18n/locales. No FROM → TO arrow — deliberate and consistent: ADR-0087 D7 says runtime-interface-only inherits the no-migration-prescription refusal, so an arrow-shaped prescription would refuse the marker. No export is removed or renamed, so no retirement row is owed; the one-line fix is present.
  • ADR-0087 disposition not-required (runtime-interface-only ui/packages/i18n/src/locales/en.ts#TranslationKeys) (changeset :11). Against D7's four predicates at head: (1) resolves — export type TranslationKeys = typeof en at locales/en.ts:5752; (2) en.ts is not *.zod.ts, not under packages/spec/src/contracts/, not an object definition (metadataSurfaceKind, gate :2823-2830); (3) not a z.input/z.infer projection; (4) its references at head (i18n.ts:14, registry.ts:54, locales/index.ts:56, react/src/index.ts:111) are none of them metadata surfaces. The other categories do not fit: type-surface-only is for a declared type moving off any (ADR-0087 addendum 2026-08-30), runtime-export-retired for a retired export — here en, the type and the ./locales/* subpaths all remain, narrower. not-required is the right disposition: objectstack migrate meta has nothing to rewrite, the compiler is the channel (the dev's reverse check, TS2339 on en.marketplace.title). The dev reports the gate verified it on origin/main; not re-run here.

③ Boundary flags

  • open_questions: [] in os-dev-report 6667819073. out_of_scope_findings: (1) the PR body's Acceptance-notes bullet "Out of scope, observed: i18n-ns-studio-en is in the eager closure … while the README says the studio namespace loads with the designer" is wrong and the seat should strike it — namespaces/studio/index.ts:33-36 imports en statically as the fallback every language resolves to, so that chunk is eager by design; (2) a language switch while a page is open unmounts the body behind the skeleton (half-filled install dialog lost) — a stated design choice, recorded in the PR body, accepted.
  • Author: objectstack-fleet[bot] (PR user.login) — no login note owed.
  • Size: additions 4,338 + deletions 2,294 = 6,632 changed lines (PR object), over the 3,000 line of AGENTS.md §7(c): the landing is a human merge — an authorized APPROVED review by a GOVERNED_APPROVERS account (scripts/pm/check-governed-queue-guard.mjs:698: os-zhuang, hotlong) and then the owning seat lands it. Stated, not a finding. PR is still a draft at this head.
  • Skills-docs: (owed by the (narrowing) arm). git grep -n "marketplace\." origin/main -- skills/ .claude/ docs/ → 17 hits: marketplace.zod.ts and marketplace prose in ADR-0019/0025/0026/0028/0048, docs/design/marketplace-publishing.md, docs/qa/platform-checklist/FOLLOW-UPS.md. None names a marketplace.* i18n key, the pack shape, or teaches that the group lives in locales/CODE.ts. .claude/skills/pm-dispatch/references/execution-duties.md:79 teaches that exporting the pack and TranslationKeys publishes every key — still true, and it is the rule this declaration obeys. docs/qa/platform-checklist/areas/platform-core.json:744 names ui/packages/i18n/src/locales/en.ts (marketplace load failed/unreachable) as a dated re-read coordinate at base fd246c22 — a record of a past reading, not teaching text; no follow-up owed. So Skills-docs: none — REASON is the right line. The body currently spells it Skills-docs: none (no skills/** text names the moved keys or the pack shape); SKILL.md's spelling is none — REASON, and the reason should also cover docs/ and .claude/ (should-fix on the body, below).

④ Design judgment

The narrowing against the published spec on main: does it take a declared capability away at a consumer, and does the pin hold every reader?

  • In-tree readers of a moved key outside the frame. Head grep marketplace\.[A-Za-z] over ui/ outside console/marketplace/, locales/, namespaces/ and tests hits only CHANGELOGs, the i18n README, the gate scripts' docblocks and vocabulary table (check-i18n-call-site-keys.mjs:1101, 1110) and the drift baseline. No source outside the directory spells a moved key. Doors into the directory from outside (head grep console/marketplace/): app-shell/src/index.ts:279-280 (the two gated pages), marketplaceApi.js (layout/AiUsageIndicator.tsx:42, layout/StorageUsageBanner.tsx:50, views/metadata-admin/PackagesPage.tsx:90), and MarketplaceRowActions.js, dependencyMissing.js, protocolCheckSkipped.js, ProtocolCheckSkippedNote.js (PackagesPage.tsx:91-94). PluginDisclosure.tsx, whose template families marketplace.category.* and marketplace.disclosure.* are in the namespace (namespaces/marketplace/en.ts:160, 208), is imported only by MarketplacePackagePage.tsx:63, under the frame. Studio's packages page therefore renders only the 19 resident leaves; no key or English shows there.
  • The boundary test (marketplaceNamespaceBoundary.test.ts) computes that census itself (:148-156), walks relative imports transitively from every ungated door (:159-168), and holds every spelled key or template head to the entry en (:207-219), with a lit control that it read more than 15 keys; it also pins that both pages' exported component returns a MarketplaceNamespaceGate element (:188-205) and that the probe key really left the pack (:221-224). It went red at the 69ab56e1 packs (dev's report). What it pins: every reader that reaches the directory by a relative import. What it does not: a file outside the directory that spells a namespace key without importing from it — none exists today, and check-i18n-call-site-keys would accept such a call site because the key is defined. That is the one residual (should-fix, below), not a broken consumer.
  • Ten languages, first paint, fallback. MARKETPLACE_LOADERS names all ten (namespaces/marketplace/index.ts:98-110), pinned to BUILT_IN_LANGUAGE_CODES by marketplace-namespace.test.ts:106-108, 117-119. resolveMarketplaceLanguage uses the registry's matchLanguageTag (registry.ts:112-117), so zh-CN → zh, and the bundle is added under zh, which is on i18next's resolution path for zh-CN as the packs already are (:198-207, 254). catalogueTargets always includes en (:204), so a leaf a translation lacks falls back to English, never a raw key. useMarketplaceNamespace answers loading until both are resident (installResident :240-258, :341-354) and the gate renders the copy-free skeleton, so a zh user's first frame with text is Chinese — MarketplacePage.namespaceGate-30588.test.tsx records every text node via a MutationObserver (:143-175) and asserts no English title/subtitle and no marketplace.X over the whole sequence (:178-210). failed is answered only when the request is not in flight (:346-349); the effect retries once per mount or language (:334-336) and the button retries on demand; the lazy test waits 50 ms and asserts one request, then two after retry() (:163-183) — no loop. A host's own resources win because addResourceBundle(…, deep=true, overwrite=false) (:254) and the per-instance installed set (:224) keeps a host-spelled key from suppressing the rest of the catalogue (lazy test case 4).
  • A key in both pack and namespace is refused in three places (locales/index.ts:75-77 at evaluation; collectEnKeys; readPacks) and pinned per language by marketplace-namespace.test.ts:132-139; the gate tests pin the refusals on fixtures (check-i18n-call-site-keys.test.ts:278-293, check-i18n-en-drift.test.ts:494-501).
  • Would the four tests go red on the regression they pin? (a) MarketplacePage.namespaceGate-30588.test.tsx: packs alone are registered (:110-111), so with the gate removed the first frame renders marketplace.title raw and :184-186 fail (dev's ablation: 2/3 and 1/3 red); case 4 (:232-275) fails without the isFrameHolding guard — the zh banner is raised at the switch (red before the fix). (b) marketplaceNamespaceBoundary.test.ts case 3 fails on any ungated key missing from the entry en. (c) marketplace-namespace-lazy.test.tsx case 1: any ready before the zh catalogue lands records a zh-CN| followed by English or a raw key entry and :129 fails; case 5 fails on a second automatic request. (d) marketplace-namespace.test.ts: a static catalogue import or a template-literal loader fails :106-115; a copied (not cut) key fails :138.
  • External (npm) consumers. A host component that reads a moved key through t() without the frame sees a raw key (or English after an earlier visit); the changeset says what to call. A host reading en.marketplace.title off the entry gets TS2339 — the compiler channel D8 names. A host that already calls registerBuiltInLocales() is unaffected (complete catalogues, no fetch). This is the declared narrowing, not a hidden one.
  • Observations, not findings: the catalogue fetch starts at page mount, after the route chunk has arrived — two sequential round trips on a first open; installResident() and loadMarketplaceCatalogue() run during render (store writes and a fetch start), idempotent through given and inFlight and the same shape as useStudioCatalogue.

Findings:

  • must-fix: none.
  • should-fix (1), the pin's residual: a marketplace.* namespace key spelled by a source outside console/marketplace/ that imports nothing from that directory is read by no pin and is accepted by check-i18n-call-site-keys (the key exists in collectEnKeys). Follow-up, not this PR: have the call-site gate, which already knows PACK_NAMESPACE_DIRS, refuse a namespace key spelled outside the directories that render under the frame, or widen the boundary census to every source outside the directory.
  • should-fix (2), PR body only, for the seat: strike the "Out of scope, observed" bullet on i18n-ns-studio-en (studio's en is resident by design, namespaces/studio/index.ts:33-36); respell the third line as Skills-docs: none — no skills/, .claude/ or docs/ text names the moved keys or teaches the pack shape (17 hits on marketplace.are allmarketplace.zod.ts and marketplace prose).

Implemented-by: claude/project-thread-9mg01c
Reviewed-by: session_01L6bhgWn6zhNa4LL7VB1rtL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

维护者速读

  • 改了什么:控制台首屏常驻的英文语言包里,市场页(应用市场列表和软件包详情)的文案搬进一个按需加载的 marketplace 命名空间,十种语言都有;打开市场页时才取,取到之前显示不带文字的骨架,不会闪英文或键名。Studio 软件包页(市场路由外)用到的 19 条英文留在常驻包里。
  • 为什么改:首屏英文包 i18n-locale-en 的 gz 上限是 60,000 字节,main 上只剩约 63 字节,任何加英文界面文字的卡都会让 Build & E2E UI 变红(PR feat(console): a marketplace app's Install confirms what it adds before it installs #30545、PR feat(console): an administrator's first-run checklist on Home replaces the currency and time zone dialog #30552 已被挡住)。本 PR 不抬上限,把读数从 59,951 降到 57,010,留出 2,990 字节。
  • 风险与回滚:第一次打开市场页多一次按需加载;切换语言时页面先回到骨架,正在填的安装对话框会丢(PR 的 Acceptance notes 写明了取舍)。@object-ui/i18n 根入口导出的 en 和 locales/<lang> 包模块少了 marketplace 子树,属于收窄;@object-ui/i18n/locales 这扇“全部语言”的门仍然给出完整目录。回滚即撤本 PR。
  • 先看:ui/packages/i18n/src/namespaces/marketplace/index.ts(加载器),ui/packages/app-shell/src/console/marketplace/MarketplaceNamespaceGate.tsx(页框),ui/packages/i18n/src/locales/index.ts(完整目录的合并),ui/scripts/check-i18n-call-site-keys.mjs。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/i18n/src/__tests__/marketplace-namespace.test.ts packages/i18n/src/__tests__/marketplace-namespace-lazy.test.tsx packages/app-shell/src/console/marketplace/__tests__/marketplaceNamespaceBoundary.test.ts packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.namespaceGate-30588.test.tsx,预期全部通过。
  • 需要你做的:本 PR 改动 6,632 行(41 文件,+4,338/−2,294,大部分是十个语言文件里整段搬移),超过 3,000 行线,要一位有权限的维护者(os-zhuang 或 hotlong)在 PR 上给 APPROVED review;批准后席位 ready 并挂自动合并。
  • 席位意见:可合。整张卡一次交付(Fixes),达档契约复核 PASS 在案。

席位核对 — ACCEPT · head 17e15c7

  • 形态:41 文件 +4,338/−2,294。check-governed-merges --pr 30640:不碰受管面;6,632 行 > 3,000,按 AGENTS.md §7(c) 走人工合并。Fixes #30588,目标 main。文件在认领 6664911761 的文件面内,只有一处偏差(见下);PER_CHUNK_GZIP_CEILINGS['i18n-locale-en'] 仍是 60_000(只加了注释),PER_CHUNK_BASELINE、locales/registry.ts、语言包文件名和 packages/** 都不在 diff 里。
  • 契约:Clause-② yes (narrowing),Metadata-protocol none,changeset @object-ui/i18n 与 @object-ui/app-shell 均 patch,标题带 !。放宽:新子路径 @object-ui/i18n/namespaces/marketplace 及其加载 API;收窄:根入口的 en、TranslationKeys 和 locales/<lang> 包模块少了 marketplace 子树。ADR-0087 处置 not-required (runtime-interface-only …#TranslationKeys)。CONTRACT_REVIEW_TIER 复核 PASS:评论 6668069937(认领 6667872046),钉本 head。Skills-docs: none — …(席位按 SKILL.md 的写法改了正文这一行,理由是 skills/、.claude/、docs/ 里 17 处 marketplace. 都是 marketplace.zod.ts 和市场散文,没有一处教这些键或包的形状)。
  • 前提实测:dev 报告 6667819073。合并基 bdbaf80c 上 i18n-locale-en 59,951 → 终 head 57,010,余量 2,990 ≥ 2,000;eager 闭包 2,204,342 → 2,201,442,103 个块名两边一致,命名空间、加载器和页框都不在首屏。Studio 软件包页会渲染 MarketplaceRowActions 等,那 19 条英文留在常驻包,由新的 boundary 测试钉住;dev 还修了本改动引入的一个问题(切语言时页框回骨架卸载页面,示例数据横幅在人还在页面时弹出),复现转红、修好、消融转红。全量 dogfood 420 文件 3,496 例绿;派生门禁 57/57 exit 0,--ran 0 NOT-MEASURED。
  • 门禁 check-runs:head 上 60 个,49 success,11 skipped(Build/Test Core、Dogfood、Temporal Conformance、Docs、UI Published Artifacts、两个 opt-in 作业与 schedule 门,ui-only diff 的预期跳过),0 红;Build & E2E UI 在这个 head 上绿。mergeable_state clean。
  • 接受的偏差:ui/packages/i18n/src/locales/index.ts 不在认领列出的文件面里,它把每个语言包和命名空间目录深合并,让 @object-ui/i18n/locales 这扇门的键集和 main 一致;复核判定方向对、不改变收窄的范围。它也是 i18n phase 2 (Console UI): ui/packages/i18n reads PLATFORM_LOCALES, adds zh-Hant, and moves pack keys to table tags #30511 / PR feat(i18n)!: the Console's language packs follow the platform locale table, keyed by its tags, and Traditional Chinese (zh-Hant) ships #30595 改的文件,那张卡已同意排在本卡之后合 main。切语言时页面回骨架、未提交的安装对话框会丢,PR 写明了取舍。正文里 dev 误记的“i18n-ns-studio-en 在首屏”一条由席位删掉:studio 的 en 按设计作为兜底常驻。
  • 范围外去向:复核的 should-fix 1(命名空间里的键若被市场页框以外、又不经 console/marketplace/ 导入的源文件读到,现有门禁都不拦)→ 已立 i18n gates: a marketplace.* namespace key read outside the marketplace pages' frame passes every gate and renders as a raw key #30645。
  • 落地条款:超 3,000 行,席位不先 ready、不先挂自动合并。等 GOVERNED_APPROVERS(os-zhuang、hotlong)之一的 APPROVED review;批准后若 head 不变,席位中继 pr_ready + automerge_enable;head 变了先复测再落。合并即关卡(Fixes),席位同一笔摘掉 pm:dispatched,并给 domain:ui#5(pm:mailbox — domain:ui#5 · os-sales #30425)和 domain:ui#6(pm:mailbox — domain:ui#6 · os-sales #30422)发 Notify: landed。

The ten packs conflicted on the marketplace group this branch moved out.
Kept this branch's packs and carried main's purgeConfirmAtMost rewrite into
namespaces/marketplace/CODE.ts; each language's pack merged with its
catalogue now equals main's pack.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXDwFw71MwSG9FKYDQ
… read the package page now makes

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXDwFw71MwSG9FKYDQ
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

维护者速读

  • 改了什么:和上一版 ACCEPT(6668161639,head 17e15c7f)相同:市场页文案从首屏常驻的语言包搬进按需加载的 marketplace 命名空间,十种语言都有。这一版只多两件事:合入了 main;把 PR feat(console): after a reload, the package page still says how many sample records a purge would delete #30653(Installed app page: after a reload, show how many sample records are left (read the new server count) instead of only yes/no #30616)新改的清除确认文案 purgeConfirmAtMost 一并搬进命名空间。
  • 为什么改:首屏英文包 i18n-locale-en 上限 60,000 gz,main 上快满了,挡住了加市场文案的 PR。合入 main 后读数 57,003,余量 2,997。
  • 风险与回滚:同上一版,回滚即撤本 PR。
  • 先看:同上一版;这一轮的增量只在合并提交 64857f64(十个语言文件)和 54db079b(一行测试 stub)。
  • 两分钟验证:git grep -n "the number this install loaded\|即本次安装载入的数量" 54db079b -- ui/packages/i18n/src/,预期 0 命中,即旧文案没有被合并带回来。
  • 需要你做的:本 PR 改动 6,633 行,超过 3,000 行线,要 os-zhuang 或 hotlong 之一在 head 54db079b 上给 APPROVED review。批准后席位转 ready 并挂自动合并。
  • 席位意见:可合。

席位核对 — ACCEPT · head 54db079

  • 形态:41 文件 +4,339/−2,294 = 6,633 行。check-governed-merges --pr 30640:0/41 条路径落在受管面,按行数走人工合并(AGENTS.md §7(c))。和 17e15c7f 相比只多两个提交:合并提交 64857f64(合入 main 47c457b23,没有 rebase),以及 54db079b(MarketplacePage.namespaceGate-30588.test.tsx 加一行 countLocalSampleData: async () => null,用来 stub feat(console): after a reload, the package page still says how many sample records a purge would delete #30653 新增的读数接口)。
  • 合并实测(席位自己量):十个命名空间文件里 purgeConfirmAtMost 的每个复数形式(ar 7、ru 5、es/fr/pt 4、de/en/ja/ko/zh 3),都和 main 47c457b23 语言包里的值逐字节一致。旧文案在 ui/packages/i18n/src/ 下 0 命中;十个常驻语言包里也没有 purgeConfirmAtMost。合并带进来的另一处 diff 只有 main 自己的 77c203a3(settingsHub 副标题),不是本 PR 的改动。
  • 契约:没变。Clause-② yes (narrowing),Metadata-protocol none,Skills-docs: none — …。这一轮只动文案值和一个测试 stub,没有新导出、新键或新子路径,所以 CONTRACT_REVIEW_TIER 复核 PASS(6668069937,钉在 17e15c7f)的结论照旧适用,不需要重做。
  • 前提实测:dev 报告 6671129796,在 54db079b 上测:
    • 每种语言的“语言包深合并命名空间”都和 main 的语言包 deep-equal。
    • console/marketplace 加 i18n 测试 150 文件、1,980 例绿;ui scripts/ 170 文件、4,979 例绿。
    • check:i18n-keys、check:i18n-dead-keys(候选和上一 head 相同)、check-i18n-en-drift.mjs --base origin/main 都 exit 0。
    • i18n-locale-en 57,003 gz(余量 2,997);命名空间、加载器和页框都不在首屏。
    • 派生门禁 57/57 exit 0,--ran 0 NOT-MEASURED;全量 dogfood 421 文件、3,513 例绿。
  • 门禁 check-runs:head 上 60 个,49 success,11 skipped(ui-only diff 的预期跳过),0 红;Build & E2E UI 绿。mergeable_state clean。席位改正文后,PR-body guards 和 Check Changeset 重跑也是绿的。
  • 正文:席位用 issue_patch 改了正文,把速读里的体积读数更新到 54db079b,并在 Tests 段加了一段 54db079b 重跑记录。尾部的签名行不变。
  • 接受的偏差:同上一版(locales/index.ts 的完整目录合并;切语言时页面回到骨架)。
  • 落地条款:同上一版。上一版 ACCEPT 钉的是 17e15c7f,作废,以本条为准。批准必须给在 54db079b 上;head 再变就先复测,再落地。合并即关卡(Fixes)。席位同一笔摘掉 pm:dispatched,并给 domain:ui#5(pm:mailbox — domain:ui#5 · os-sales #30425)和 domain:ui#6(pm:mailbox — domain:ui#6 · os-sales #30422)发 Notify: landed。

This was referenced Nov 5, 2026
@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 6, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

Contract-review-claim: session_01L6bhgWn6zhNa4LL7VB1rtL · head 54db079 · 2026-11-06T03:56Z

The CONTRACT_REVIEW_TIER PASS 6668069937 is pinned to 17e15c7f; the hop to 54db079b (merge of main 47c457b23 plus the ported purgeConfirmAtMost values and a test stub) is not prose-only, so the narrowing face owes a record on this head before the approvers are asked.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 54db079b29c31bc40b8155c3f0db7a0ac4d683a3
Local-runs: none
Scope: ui/apps/console/vite.config.ts, ui/packages/app-shell/src/console/marketplace/MarketplaceNamespaceGate.tsx, ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx, ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx, ui/packages/app-shell/src/console/marketplace/SampleDataLeaveBanner.tsx, ui/packages/app-shell/src/console/marketplace/marketplaceFrameHolding.ts, ui/packages/app-shell/src/console/marketplace/tests/MarketplacePage.namespaceGate-30588.test.tsx, ui/packages/app-shell/src/console/marketplace/tests/marketplaceNamespaceBoundary.test.ts, ui/packages/i18n/README.md, ui/packages/i18n/package.json, ui/packages/i18n/src/tests/marketplace-namespace-lazy.test.tsx, ui/packages/i18n/src/tests/marketplace-namespace.test.ts, ui/packages/i18n/src/locales/index.ts, ui/packages/i18n/src/locales/en.ts, ui/packages/i18n/src/locales/zh.ts, ui/packages/i18n/src/locales/ja.ts, ui/packages/i18n/src/locales/ko.ts, ui/packages/i18n/src/locales/de.ts, ui/packages/i18n/src/locales/fr.ts, ui/packages/i18n/src/locales/es.ts, ui/packages/i18n/src/locales/pt.ts, ui/packages/i18n/src/locales/ru.ts, ui/packages/i18n/src/locales/ar.ts, ui/packages/i18n/src/namespaces/marketplace/index.ts, ui/packages/i18n/src/namespaces/marketplace/en.ts, ui/packages/i18n/src/namespaces/marketplace/zh.ts, ui/packages/i18n/src/namespaces/marketplace/ja.ts, ui/packages/i18n/src/namespaces/marketplace/ko.ts, ui/packages/i18n/src/namespaces/marketplace/de.ts, ui/packages/i18n/src/namespaces/marketplace/fr.ts, ui/packages/i18n/src/namespaces/marketplace/es.ts, ui/packages/i18n/src/namespaces/marketplace/pt.ts, ui/packages/i18n/src/namespaces/marketplace/ru.ts, ui/packages/i18n/src/namespaces/marketplace/ar.ts, ui/scripts/tests/check-i18n-call-site-keys.test.ts, ui/scripts/tests/check-i18n-en-drift.test.ts, ui/scripts/check-eager-closure-budget.mjs, ui/scripts/check-i18n-call-site-keys.mjs, ui/scripts/check-i18n-dead-keys.mjs, ui/scripts/check-i18n-en-drift.mjs + .changeset/30588-marketplace-lazy-namespace.md (prose face, read for ②), ui/packages/i18n/src/index.ts, ui/packages/i18n/src/i18n.ts, ui/packages/i18n/src/locales/registry.ts, ui/packages/i18n/src/tests/all-locales-key-parity.test.ts, ui/packages/app-shell/src/index.ts, ui/packages/app-shell/src/console/AppContent.tsx, ui/packages/app-shell/src/services/builtinComponents.tsx, ui/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx, ui/packages/app-shell/src/console/marketplace/MarketplaceRowActions.tsx, ui/packages/app-shell/src/console/marketplace/dependencyMissing.ts, ui/packages/app-shell/src/console/marketplace/protocolCheckSkipped.ts, ui/packages/app-shell/src/console/marketplace/ProtocolCheckSkippedNote.tsx, ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts, ui/packages/app-shell/src/layout/AiUsageIndicator.tsx, ui/packages/app-shell/src/layout/StorageUsageBanner.tsx, ui/scripts/check-eager-locale-catalogues.mjs, ui/vitest.setup.i18n-catalogues.ts, ui/AGENTS.md, .github/workflows/ci.yml, docs/adr/0087-metadata-protocol-upgrade-contract.md, AGENTS.md

This record is owed on 54db079b: the PASS 6668069937 is pinned to 17e15c7f, and the two commits since (64857f64, the merge of main 47c457b23; 54db079b, a test stub) move ten locale files. Judged as the whole net diff against the merge base 47c457b23 (41 files, +4,339 / −2,294), not the last hop.

① Derived judgments

  1. What widens. One published subpath, ./namespaces/marketplace (ui/packages/i18n/package.json:41-45, the ./namespaces/studio shape), and the API behind it in ui/packages/i18n/src/namespaces/marketplace/index.ts: MarketplaceCatalogue (:81), MARKETPLACE_NAMESPACE (:84), MARKETPLACE_NAMESPACE_PROBE_KEY (:91), MARKETPLACE_LANGUAGE_CODES (:113), resolveMarketplaceLanguage() (:125), isMarketplaceCatalogueLoaded() (:158), loadMarketplaceCatalogue() (:168), MarketplaceNamespaceState (:261), useMarketplaceNamespace() (:296); ten partial packs namespaces/marketplace/CODE.ts (const en = { marketplace: {…} } as const; export default en). @object-ui/app-shell's entry gains nothing: ui/packages/app-shell/src/index.ts:279-280 still exports only the two pages; MarketplaceNamespaceGate and marketplaceFrameHolding are not on it. Right.
  2. What narrows. The entry's en (ui/packages/i18n/src/index.ts:123, a re-export of ./locales/en.js), TranslationKeys = typeof en (locales/en.ts, re-exported by src/index.ts, src/i18n.ts, locales/registry.ts, locales/index.ts:56), and each pack module @object-ui/i18n/locales/CODE: the marketplace group is cut to 19 leaves (locales/en.ts:4928-4962: installedAdditiveNote, notLoaded.{badge,protocolIncompatible,otherReason}, action.{uninstall,uninstalling,details}, uninstall.{confirm,confirmNotLoaded,successInList,successNotLoaded}, install.dependencyMissing.{heading,absent,outOfRange,unreadableRange,unreadableVersion,unreadableDeclaration}, install.protocolCheckSkipped.{noRange,unparsedRange}; the same 19 in all ten packs, read from each file). A t('marketplace.…') call keeps compiling; what narrows is the object a direct reader indexes and the type derived from it. Right, and the changeset names exactly these three.
  3. Merge correctness, ten locales, measured on the tree. For each of the ten languages I read the top-level marketplace block of the pack at the merge base 47c457b23 and, at 54db079b, the pack's residual block plus the namespace file's block, as dotted key path → value line (whitespace-stripped, comments dropped): en 193 = 193, zh 193 = 193, ja 191 = 191, ko 191 = 191, de 191 = 191, fr 197 = 197, es 197 = 197, pt 197 = 197, ru 203 = 203, ar 215 = 215 — zero differing lines in every language, no key path twice, every value byte-identical. marketplace.detail.purgeConfirmAtMost from feat(console): after a reload, the package page still says how many sample records a purge would delete #30653 is in the namespace with every plural form (en 3 forms, ar 7 forms, read directly). Outside the moved block each pack differs from the merge base by the same 8-line comment (locales/en.ts:4920-4927) and nothing else. origin/main (177f0ee2) has moved only namespaces/studio/{en,zh}.ts since the merge base; the packs and the marketplace readers are untouched, so no further port is owed at landing.
  4. The @object-ui/i18n/locales door. locales/index.ts:35-54 imports the ten packs and the ten catalogues; complete() (:67-84) deep-merges and throws on a leaf in both (:75-77); en…ar (:89-98) and builtInLocales carry the complete objects, so all-locales-key-parity.test.ts:104 and registerBuiltInLocales() keep walking the moved keys. The root entry (src/index.ts:113-123) imports ./locales/registry.js and ./locales/en.js, never ./locales/index.js, so the door stays off the eager path and the catalogues cannot ride in through it. Right; neither widening nor narrowing at this door (its key set equals main's).
  5. Arm and lines. Clause-②: yes (narrowing) and Metadata-protocol: none on the PR body and the changeset (:8-9); Skills-docs: none — REASON on the body in SKILL.md's spelling. Right: a new subpath widens, the entry en / TranslationKeys / ./locales/CODE narrow, no authorable metadata moves.
  6. Pages, frame, banner. MarketplacePage.tsx:314-322 and MarketplacePackagePage.tsx:171-179 return their bodies inside MarketplaceNamespaceGate; the gate (MarketplaceNamespaceGate.tsx:39-57) renders the copy-free MarketplaceResolving while loading, resident console.errors.somethingWentWrong / tryAgain with retry when failed (:59-75), children when ready. The holding flag is set in a useLayoutEffect with no deps (:42-44), i.e. in the layout phase of the commit that swaps the body out, which precedes that commit's passive unmount cleanups, so SampleDataLeaveBanner.tsx:49-50 reads it correctly; when the route is left the gate unmounts without re-rendering and the flag stays false, so the banner is raised. isHolding is useCallback(…, []), so the banner effect's new dep (:70) never re-runs it. Right.
  7. Chunking, read off CI not the body. vite.config.ts:1265-1276: i18n-ns-marketplace (loader, includeDependenciesRecursively: false), ten i18n-ns-marketplace-CODE, marketplace-namespace-gate (includeDependenciesRecursively: false). Both pages are lazy routes through routeChunk.lazy with a dynamic import() (AppContent.tsx:159-160, :182-183), while PackagesPage is eager (services/builtinComponents.tsx:22, :141) and imports MarketplaceRowActions, dependencyMissing, protocolCheckSkipped, ProtocolCheckSkippedNote, marketplaceApi (PackagesPage.tsx:90-94) — the co-tenancy the vite comment names. The head's Build & E2E UI (job 127555991291, the PR merge ref): eager closure 103 of 2412 chunks, 2,204,106 B gz; i18n-locale-en 55.7 KB under the 58.6 KB ceiling, headroom 2.9 KB (about 2,970 bytes). main at the merge base (job 127552106067): 103 of 2400 chunks, 2,206,749 B gz; i18n-locale-en 58.5 KB, headroom 0.1 KB. Same eager count on both sides, exactly 12 chunks added (ten catalogues 3.68–5.03 KB gz, loader 1.27 KB, gate 0.66 KB, all present in the asset list), eager bytes down by 2,643 B: nothing of the namespace is eager. The card's bar (headroom ≥ 2,000 B, ceiling untouched) holds on CI's own reading; PER_CHUNK_GZIP_CEILINGS['i18n-locale-en'] is still 60_000 and the diff to that file is the comment at check-eager-closure-budget.mjs:2229-2238; PER_CHUNK_BASELINE, locales/registry.ts and the pack file names are not in the diff.
  8. Gates learn the namespace. PACK_NAMESPACE_DIRS / PACK_DEFINITION_DIRS (check-i18n-call-site-keys.mjs:1329, :1332); collectEnKeys (:1353) reads locales/en.ts plus each namespace en.ts when present (:1361-1364) and throws on a leaf in two homes or a leaf-vs-group clash (:1406-1414); the dead-keys sweep treats hits inside PACK_DEFINITION_DIRS as non-evidence (check-i18n-dead-keys.mjs:468-474, :670); readPacks merges each language's partial packs and throws on a duplicate, skipping a partial absent at the base (check-i18n-en-drift.mjs:366-403). check:i18n-keys and check:i18n-drift run in ci.yml:3636, :3639; check:eager-closure at :4530. The en-drift ledger entry for marketplace.searchPlaceholder is now read through readPacks (check-i18n-en-drift.test.ts:662-665). Right; no rule weakened.

② Semver level

  • @object-ui/i18n: patch, @object-ui/app-shell: patch (changeset :2-3). Right under AGENTS.md Post-Task Checklist step 3: Metadata-protocol: none takes patch; a non-protocol change, widening or breaking, ships patch with its BREAKING banner and ADR-0087 disposition.
  • The BREAKING banner (changeset :20) names the three narrowed surfaces and the migration in prose: a component outside the marketplace pages calls useMarketplaceNamespace() and renders once ready; code reading the catalogue objects reads @object-ui/i18n/locales, which is complete. No export is removed or renamed, so no retirement row or tombstone is owed. No FROM → TO arrow, consistent with runtime-interface-only inheriting the no-migration-prescription refusal (ADR-0087 :751-765, :868).
  • ADR-0087 marker (changeset :11): not-required (runtime-interface-only ui/packages/i18n/src/locales/en.ts#TranslationKeys). The symbol resolves at head (export type TranslationKeys = typeof en, locales/en.ts), is not a Zod schema, a packages/spec contract or a stored-metadata projection, and its readers are the i18n runtime and @object-ui/react's re-export. check-adr-0087-registration is inside Lint & Repo Gates (success) and Check Changeset is success ×3 on this head. Right disposition; objectstack migrate meta has nothing to rewrite, the compiler is the channel.

③ Boundary flags

  • open_questions: [] in both os-dev-reports (6667819073, 6671129796). out_of_scope_findings of the first: (1) the wrong i18n-ns-studio-en bullet — struck; the body at this head carries no such bullet; (2) a language switch unmounts the page body behind the skeleton (a half-filled install dialog is lost) — a stated design choice, recorded under Acceptance notes, accepted. The second report has none.
  • Author objectstack-fleet[bot], head repo = base repo (not a fork). Draft. 6,633 changed lines, over the 3,000 line: AGENTS.md §7(c), the landing is an authorized APPROVED review by a GOVERNED_APPROVERS account on this head, then the owning seat. Stated, not a finding.
  • Check-runs on 54db079b: 65, 0 failed, 0 pending; all ten required contexts success (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Lint UI, Type Check UI, Test UI, Build & E2E UI, Live E2E UI); the skipped ones are the ui-only-diff expected skips (Docs, Temporal, Published Artifacts, opt-ins, the schedule gate).
  • Governed surfaces: none of the 41 paths is on the register (ui/AGENTS.md, .claude/**, workflows untouched).
  • ui/AGENTS.md's i18n evidence rule (u flag + \p{L} for typographic censuses) is not engaged: the new tests' ASCII classes (/\bmarketplace\.[A-Za-z]/, /^marketplace\.[\w.-]+$/) match i18n KEY identifiers, which are ASCII by construction, not translated text. ADR-0060: marketplace is a generic namespace name, not a product word; product-neutral-renderer.ratchet is inside Test UI (success).

④ Design judgment

The narrowing against the published surface on main: does any consumer lose a declared capability, and does anything eager now render a key that is only in the lazy namespace?

  • Eager and first-screen surfaces. Head grep of marketplace. + letter over ui/ outside packages/i18n, console/marketplace/, tests, scripts and prose: zero hits; over packages/, apps/, examples/: zero code readers of @object-ui/i18n (prose mentions only). Property-chain reads (.marketplace.title and the other moved families): zero. Doors into console/marketplace/ from outside: the two gated pages (app-shell/src/index.ts:279-280), cloudConsoleUrl only (layout/AiUsageIndicator.tsx:42, layout/StorageUsageBanner.tsx:50), and Studio's eager PackagesPage.tsx:90-94. What that page can reach, read file by file: MarketplaceRowActions.tsx (11 keys, its one in-directory import is ./marketplaceApi.js at :40), dependencyMissing.ts (6), protocolCheckSkipped.ts (2), ProtocolCheckSkippedNote.tsx (no key, no in-directory import), marketplaceApi.ts (no key; a type-only import of ./protocolCheckSkipped.js): 19 keys, exactly the 19 resident leaves of item ①2. Nothing eager renders a moved key, so no raw key and no English flash on any first-load surface. marketplaceNamespaceBoundary.test.ts:148-168, :207-219 computes this census from the sources and holds it; MarketplacePage.namespaceGate-30588.test.tsx:143-175 records every text node a page adds and holds "never English, never a raw key" over the whole sequence for zh-CN and de, the failed-fetch path for ar, and the no-banner-on-switch case.
  • Ten languages, fallback, loop. MARKETPLACE_LOADERS names all ten (namespaces/marketplace/index.ts:98-110), pinned to BUILT_IN_LANGUAGE_CODES. resolveMarketplaceLanguage uses the registry's matchLanguageTag (registry.ts:112-117: exact tag, then base), so zh-CN is served by zh and the bundle is added under zh; createI18n sets fallbackLng: 'en' and no load option (i18n.ts:250, :301), so i18next's default resolution path zh-CN → zh → en reaches both. catalogueTargets (:202-207) always includes en, so a leaf a translation lacks falls back to English, never to a raw key. installResident (:240-258) adds the bundle deep and without overwrite, so a host's resources win, and the per-instance installed set (:224) keeps a host-spelled key from suppressing the rest. failed is answered only when nothing is in flight (:346-349); the mount/language effect retries once (:334-336), the button on demand: no loop. In production the registry holds packs alone (preloadBootstrapLocale), so residentCatalogue() (:145-155) finds no marketplace.title and fetches; a host that called registerBuiltInLocales() is served without a fetch.
  • Would the pins go red on the regression they guard? Gate removed from a page: the first frame renders marketplace.title raw, namespaceGate-30588.test.tsx:184-186 fails (dev's ablation 2/3 and 1/3 red, consistent with the code). Holding guard removed: case 4 raises the zh banner on the switch (:233-275). A key copied, not cut: marketplace-namespace.test.ts:138 fails, and locales/index.ts:75-77, collectEnKeys and readPacks all throw. A static catalogue import or a template-literal loader: :106-115 fails. The boundary test went red at the 69ab56e1 packs (dev's report) and its lit control requires more than 15 keys read.
  • External (npm) consumers. A host reading en.marketplace.title off the entry gets TS2339; a host component reading a moved key through t() without the frame sees a raw key or, after an earlier visit, English — the changeset says what to call; a host that registers the all-ten door is unaffected. This is the declared narrowing, not a hidden one; no in-tree consumer loses anything.
  • Semantics kept. The all-ten door, parity, drift and call-site gates all still see every key; registerBuiltInLocales() still makes every string resident; text unchanged in all ten languages (item ①3). The capability the card declares — moved text displayed in all ten languages with a loading state and no key or English flash — is delivered in the tree, not only claimed.

Findings:

  • must-fix: none.
  • should-fix (1), follow-up, not this PR: nothing in CI pins that the twelve new chunks stay lazy. check-eager-locale-catalogues.mjs:108 keys on i18n-locale- only, and check-eager-closure-budget.mjs pins bytes (aggregate headroom 279 KB), not chunk names; a later static import of MarketplaceNamespaceGate from an eager surface, or a vite group edit, would make the loader plus the en catalogue (about 5 KB gz) eager without any gate going red, and the i18n-locale-en ceiling would not see it because those bytes are no longer in that chunk. Extend the locale-catalogue census to i18n-ns-marketplace-* (all ten lazy, en included — unlike the studio namespace) or pin the eager chunk count/name set. Distinct from i18n gates: a marketplace.* namespace key read outside the marketplace pages' frame passes every gate and renders as a raw key #30645 (the call-site gate's residual).
  • should-fix (2), prose only, for the seat at landing: the readings in the vite comment (vite.config.ts:1247-1261) and beside the ceiling (check-eager-closure-budget.mjs:2233-2238: 57,010 / 2,201,440 at the card's head) are the dev's local builds; CI's merge-ref readings on 54db079b are 55.7 KB (about 57,040 B) and 2,204,106 B. Neither changes a verdict; a reader of those comments should not expect the file to match CI to the byte.

Implemented-by: claude/project-thread-9mg01c
Reviewed-by: session_01L6bhgWn6zhNa4LL7VB1rtL

VERDICT: PASS

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console first load: the English language pack has 63 bytes left under its size ceiling, so any card that adds English console text fails CI

1 participant